#!/bin/bash

set -e -o pipefail

XFSTESTS_FLAVOR=android
t=$(echo ${XFSTESTS_FLAVOR}_xfstests_dir | tr "[:lower:]" "[:upper:]")
eval DIR="\$$t"
if test -z "$DIR"
then
    DIR="$(dirname "$(readlink -f "$0")")"
fi
if test ! -f "$DIR/util/get-config"
then
    echo "$(basename "$0"): couldn't find $DIR/util/get-config"
    exit 1
fi

. "$DIR/util/get-config"
. "$DIR/util/parse_opt_funcs"
. "$DIR/util/arch-funcs"

case "$1" in
    install-kconfig)
	shift
	if test ! -x "$DIR/util/install-kconfig"
	then
	    echo "Kernel configuration not supported in this installation"
	    exit 1
	fi
	export GCE_XFSTESTS_DIR="$DIR"
	XFSTESTS_FLAVOR=$XFSTESTS_FLAVOR "$DIR/util/install-kconfig" "$@"
	exit $?
	;;
    kbuild)
	shift
	if test ! -x "$DIR/util/kbuild"
	then
	    echo "kbuild not supported in this installation"
	    exit 1
	fi
	export GCE_XFSTESTS_DIR="$DIR"
	XFSTESTS_FLAVOR=$XFSTESTS_FLAVOR "$DIR/util/kbuild" "$@"
	exit $?
	;;
esac

# Chroot directory on device.
# Note: this will be wiped clean when deploying a new chroot tarball.
CHROOT_DIR="/data/xfstests-chroot"

# Results directory.
RESULTS_DIR="/data/xfstests-results"

. "$DIR/util/parse_cli"

if test -n "$SKIP_LOG" ; then
    LOGFILE=/tmp/log.$(date +%Y%m%d%H%M)
else
    mkdir -p "$DIR/logs"
    LOGFILE="$DIR/logs/log.$(date +%Y%m%d%H%M)"
fi

die()
{
    echo -e 1>&2 "[ERROR] android-xfstests: $*"
    exit 1
}

ask_yesno()
{
    local response
    echo -n -e "$@ (y/N) "
    read response
    if [ "$response" != y ]; then
	exit 1
    fi
}

adb_ready()
{
    adb devices | grep -E -q '(device|recovery)$'
}

fastboot_ready()
{
    fastboot devices | grep -q 'fastboot$'
}

wait_for_device()
{
    local want_adb=false
    local want_fastboot=false
    local waiting_for=""
    local unauthorized=false
    local waiting=false

    if [[ ,$1, == *,adb,* ]]; then
	want_adb=true
	waiting_for="adb to be ready"
    fi
    if [[ ,$1, == *,fastboot,* ]]; then
	want_fastboot=true
	waiting_for+="${waiting_for:+ or for }device to enter fastboot mode"
    fi
    : "${waiting_for:=device}"

    while true; do
	if $want_adb; then
	    if adb_ready; then
		break
	    fi
	    if ! $unauthorized && adb devices | grep -q 'unauthorized$'; then
		echo "adb is not authorized.  Authorize it using the dialog on the device to continue."
		unauthorized=true
	    fi
	fi
	if $want_fastboot && fastboot_ready; then
	    return
	fi
	if ! $waiting && ! $unauthorized; then
	    echo "Waiting for $waiting_for..."
	    waiting=true
	fi
	sleep 0.5
    done

    # Make sure adbd is running as root and that SELinux is in permissive mode.
    if ! adb root > /dev/null ; then
	die "Unable to restart adbd as root on the device.  Maybe your device is not rooted?"
    fi
    adb shell "setenforce 0"
}

wait_for_adb()
{
    wait_for_device adb
}

wait_for_fastboot()
{
    wait_for_device fastboot
}

wait_for_adb_or_fastboot()
{
    wait_for_device adb,fastboot
}

reboot_into_fastboot_mode()
{
    adb reboot bootloader
    wait_for_fastboot
}

# Query the version of the kernel running on the device
query_kernel_version()
{
    adb shell "uname -r -v"
}

# Try to extract the version information from the $KERNEL image by grepping for
# the linux_banner[] string.  It's a hack, but there doesn't seem to be a better
# way, and scripts elsewhere supposedly have been doing this for a long time...
extract_kernel_version()
{
    local decompress

    # Note: we use the filename extension rather than the 'file' program to get
    # the compression format because old versions of 'file' don't recognize
    # LZ4-compressed files.
    case "$(basename "$KERNEL")" in
    Image.gz*)
	decompress="gzip -d -c"
	;;
    Image.bz2*)
	decompress="bzip2 -d -c"
	;;
    Image.xz*)
	decompress="xz -d -c"
	;;
    Image.lz4*)
	decompress="lz4 -d" # no -c option; stdout is assumed when not a tty
	;;
    *)
	decompress="cat"
	;;
    esac
    local banner="$($decompress "$KERNEL" \
	| grep -a -m1 'Linux version [0-9]\+\.[0-9]\+.*#.*$')"

    if [ -n "$banner" ]; then
	local krelease="$(echo "$banner" | awk '{print $3}')"
	local kver="#${banner##*#}"
	echo "$krelease $kver"
    fi
}

# If the specified $KERNEL isn't already running on the device, try to boot it
# using 'fastboot boot'.
boot_kernel()
{
    local version actual_version
    local have_version=true

    if [ ! -f "$KERNEL" ]; then
	die "The specified kernel image does not exist: $KERNEL"
    fi

    version="$(extract_kernel_version "$KERNEL")"
    if [ -z "$version" ]; then
	cat 1>&2 <<EOF
Warning: unable to extract version information from $KERNEL.
We won't be able to verify that the device has successfully booted the kernel!
EOF
	version="(unknown version)"
	have_version=false
    fi

    wait_for_adb_or_fastboot
    if adb_ready; then
	actual_version="$(query_kernel_version)"
	if $have_version && [ "$version" = "$actual_version" ]; then
	    # Kernel is already running.
	    return
	fi
	echo "Rebooting to start new kernel: $version"
	stop_existing_tests
	reboot_into_fastboot_mode
    else
	echo "Starting kernel: $version"
    fi
    fastboot boot "$KERNEL"
    wait_for_adb

    actual_version="$(query_kernel_version)"
    if $have_version && [ "$version" != "$actual_version" ]; then
	 die "Kernel did not successfully boot!\n" \
	     "Expected: $version\n" \
	     "Actual: $actual_version\n"
    fi
}

chroot_prepare()
{
    cat <<EOF | adb shell
! mountpoint $CHROOT_DIR/sys > /dev/null && mount sysfs -t sysfs $CHROOT_DIR/sys
! mountpoint $CHROOT_DIR/proc > /dev/null && mount proc -t proc $CHROOT_DIR/proc
! mountpoint $CHROOT_DIR/dev > /dev/null && mount --bind /dev $CHROOT_DIR/dev
! mountpoint $CHROOT_DIR/dev/pts > /dev/null && mount --bind /dev/pts $CHROOT_DIR/dev/pts

# Allow xfstests to detect that SELinux is in use.
! mountpoint $CHROOT_DIR/sys/fs/selinux > /dev/null && \
		mount selinuxfs -t selinuxfs $CHROOT_DIR/sys/fs/selinux
touch $CHROOT_DIR/etc/selinux/config
if [ ! -e $CHROOT_DIR/usr/sbin/selinuxenabled ]; then
	ln $CHROOT_DIR/bin/true $CHROOT_DIR/usr/sbin/selinuxenabled
fi

# 'mountpoint' doesn't work with directory bind mounts; use /proc/mounts instead
if ! cut -d' ' -f2 /proc/mounts 2>/dev/null | grep -q '^$CHROOT_DIR/results$'; then
    mkdir -p $RESULTS_DIR
    mount --bind $RESULTS_DIR $CHROOT_DIR/results
fi

# /dev/fd needs to exist in order for bash process substitution to work.
if [ ! -e /dev/fd ]; then
	ln -s /proc/self/fd /dev/fd
fi

# Android puts loopback device nodes in /dev/block/ instead of /dev/.
# But losetup can only find them in /dev/, so create them there too.
for i in \`seq 0 7\`; do
	if [ ! -e /dev/loop\$i ]; then
		mknod /dev/loop\$i b 7 \$i
	fi
done
EOF
}

chroot_wipe()
{
    cat <<EOF | adb shell
umount $CHROOT_DIR/sys/fs/selinux &> /dev/null
umount $CHROOT_DIR/sys &> /dev/null
umount $CHROOT_DIR/proc &> /dev/null
umount $CHROOT_DIR/dev/pts &> /dev/null
umount $CHROOT_DIR/dev &> /dev/null
umount $CHROOT_DIR/results &> /dev/null
rm -rf $CHROOT_DIR
mkdir $CHROOT_DIR
EOF
}

# When entering a login shell, we need to override $PATH to allow commands in
# /etc/profile to run, as they fail with the Android $PATH.  The $PATH will,
# however, get overridden again when /root/.bashrc is sourced.  (It's fine to do
# this for noninteractive shell commands too.)
CHROOT_CMD="HOME=/root TMPDIR=/tmp PATH=\$PATH:/usr/sbin:/usr/bin:/sbin:/bin chroot $CHROOT_DIR"

chroot_run()
{
    adb shell "$CHROOT_CMD /bin/bash -c \"$*\""
}

chroot_interactive_shell()
{
    # The -t option ("force PTY allocation") makes the shell interactive even
    # though we're passing 'adb shell' a command to run.  This allows us to
    # start a shell in the chroot.
    adb shell -t "$CHROOT_CMD /bin/sh -c \"cd ~; /bin/bash --login\""
}

setup_chroot()
{
    if ! [ -f "$ROOT_FS" ]; then
	echo "The xfstests chroot tarball does not exist:"
	echo "        $ROOT_FS"
	if [ -z "$ROOT_FS_URL" ]; then
	    exit 1
	fi
	ask_yesno "Would you like to download the latest public tarball to that location?"
	wget -O "$ROOT_FS" "$ROOT_FS_URL"
	echo "Finished downloading chroot tarball."
    fi
    local old_md5sum="$(adb shell '[ -e '$CHROOT_DIR'/chroot_md5sum ] &&
					cat '$CHROOT_DIR'/chroot_md5sum')"
    local new_md5sum="$(md5sum "$ROOT_FS" | cut -d' ' -f1)"
    if [ "$old_md5sum" = "$new_md5sum" ]; then
	chroot_prepare
	return 0
    fi

    echo "Deploying chroot tarball to device (path=$ROOT_FS, md5sum=$new_md5sum)..."
    stop_existing_tests
    chroot_wipe

    # If the chroot tarball is in .tar.xz format, then decompress it host-side,
    # since Android devices don't usually include the xz program.
    local srcfile="$ROOT_FS"
    local decompress="cat"
    if file "$ROOT_FS" | grep -q '\<XZ compressed\>'; then
	xz -d -c "$ROOT_FS" > "$tmpfile"
	srcfile="$tmpfile"
    elif file "$ROOT_FS" | grep -q '\<gzip compressed\>'; then
	decompress="gzip -d -c"
    fi

    local destfile=$CHROOT_DIR/"$(basename "$ROOT_FS")"
    adb push "$srcfile" "$destfile"
    cat <<EOF | adb shell
$decompress "$destfile" | tar -C $CHROOT_DIR -x
mv $CHROOT_DIR/root/test-config $CHROOT_DIR/root/test-config.orig
echo $new_md5sum > $CHROOT_DIR/chroot_md5sum
EOF
    adb push "$DIR/test-appliance/android-test-config" "$CHROOT_DIR/root/test-config"
    chroot_prepare
}

try_shrink_userdata()
{
    local fs_type

    cat <<EOF

It seems you haven't run android-xfstests on this device yet, so
there isn't any space for the xfstests partitions.  Would you like
to make space for them by reformatting the userdata filesystem with
a smaller size?  WARNING: THIS WILL DELETE ALL USER DATA!

EOF
    ask_yesno "    Erase and reformat userdata with smaller size?"
    echo

    # The filesystem type (e.g. "ext4" or "f2fs") for 'fastboot format' defaults
    # to the value of the bootloader variable "partition-type:userdata".  But on
    # some devices this is set to "raw" which doesn't work.  Instead, just
    # specify the filesystem type which the device is already using.
    fs_type=$(adb shell "cat /proc/mounts" | awk '$2 == "/data" {print $3}')

    reboot_into_fastboot_mode

    fastboot "format:$fs_type:0x100000000" userdata # 4 GiB
}

setup_partitions()
{
    adb push $DIR/test-appliance/android-setup-partitions \
	    $CHROOT_DIR/setup-partitions > /dev/null
    adb shell "rm -f $CHROOT_DIR/setup-partitions-result"
    chroot_run /setup-partitions
    echo "unknown" > "$tmpfile"
    adb pull $CHROOT_DIR/setup-partitions-result "$tmpfile" &> /dev/null || true
    local result="$(<"$tmpfile")"
    case "$result" in
    ready)
	;;
    shrink_userdata)
	return 1
	;;
    insufficient_space)
	die "This device doesn't have enough space on its internal storage to run android-xfstests."
	;;
    *)
	die "An unexpected problem occurred while setting up the xfstests partitions."
	;;
    esac
    return 0
}

xfstests_running()
{
    [ -n "$(adb shell 'pgrep runtests\.sh')" ]
}

# If xfstests is already running, ask the user if they want to terminate it
stop_existing_tests()
{
    if ! xfstests_running; then
	return 0
    fi
    ask_yesno "xfstests is already running!  Terminate it?"
    adb shell "pkill -f \"bash \./check\""
    local start=$(date +%s)
    while (( $(date +%s) <= start + 10 )); do
	local existing=$(adb shell 'pgrep runtests\.sh')
	if [ -z "$existing" ]; then
	    return 0
	fi
	sleep 1
    done
    die "Failed to stop existing xfstests instance."
}

tmpfile="$(mktemp)"
trap "rm -f \"$tmpfile\"" EXIT

if ! type -P adb > /dev/null;  then
    die "adb is not installed"
fi

if ! type -P fastboot > /dev/null ; then
    die "fastboot is not installed"
fi

case "$ARG" in
    cmd=shell*|cmd=maint*)
	want_shell=true
	;;
    *)
	want_shell=false
	if adb_ready; then
	    stop_existing_tests
	fi
	;;
esac

# Set up the kernel, the chroot, and the xfstests partitions.

tried_to_shrink_userdata=false
while true; do

    # Start by booting into the correct kernel.
    if [ -n "$KERNEL" ]; then
	boot_kernel
    elif fastboot_ready; then
	fastboot continue
    fi

    wait_for_adb

    # Set up the chroot and xfstests partitions.  Note: if an interactive shell
    # is requested and tests are currently running, we won't mess around with
    # the partitions.  However, we'll still try to set up the chroot just in
    # case a different ROOT_FS was specified (in which case the existing tests
    # will need to be stopped).
    setup_chroot
    if $want_shell && xfstests_running; then
	break
    fi
    if setup_partitions; then
	break
    fi

    # Need to shrink userdata to make space for the xfstests partitions!
    if $tried_to_shrink_userdata; then
	die "An unexpected problem occurred when shrinking userdata."
    fi
    try_shrink_userdata
    tried_to_shrink_userdata=true

    # 'userdata' has just been formatted and the device is now in fastboot mode.
    # Start the configuration over again.
done

if $want_shell; then
    chroot_interactive_shell
    exit 0
fi

cat > "$tmpfile" <<EOF
#!/bin/bash

cd /root
. test-config

export FSTESTAPI="$(echo $API | sed -e 's/\./ /g')"
export FSTESTCFG="$(echo $FSTESTCFG | sed -e 's/,/ /g')"
export FSTESTSET="$(echo $FSTESTSET | sed -e 's/,/ /g')"
export FSTESTOPT="$(echo $FSTESTOPT | sed -e 's/,/ /g')"
export FSTESTTYP="$PRIMARY_FSTYPE"
export MNTOPTS="$MNTOPTS"
export FSTESTEXC="$(echo $FSTESTEXC | sed -e 's/,/ /g')"

umount \$PRI_TST_MNT &> /dev/null
umount \$SM_TST_MNT &> /dev/null
umount \$SM_SCR_MNT &> /dev/null
umount \$LG_TST_MNT &> /dev/null
umount \$LG_SCR_MNT &> /dev/null

./runtests.sh
EOF
adb push "$tmpfile" $CHROOT_DIR/run-xfstests > /dev/null
adb shell "chmod +x $CHROOT_DIR/run-xfstests"

chroot_run /run-xfstests |& tee $LOGFILE

if test -n "$SKIP_LOG" ; then
    rm $LOGFILE
else
    sed -i -e '/^-------------------- Summary report/,$d' $LOGFILE
    echo "logfile in $LOGFILE"
fi
